import { StyleString, UIOptions } from './LSPlugin'
import { PluginLocal } from './LSPlugin.core'
import { snakeCase } from 'snake-case'
import * as path from 'path'

interface IObject {
  [key: string]: any;
}

declare global {
  interface Window {
    api: any
    apis: any
  }
}

export const IS_DEV = process.env.NODE_ENV === 'development'

let _appPathRoot

export async function getAppPathRoot (): Promise<string> {
  if (_appPathRoot) {
    return _appPathRoot
  }

  return (_appPathRoot =
      await invokeHostExportedApi('_callApplication', 'getAppPath')
  )
}

export async function getSDKPathRoot (): Promise<string> {
  if (IS_DEV) {
    // TODO: cache in preference file
    return localStorage.getItem('LSP_DEV_SDK_ROOT') || 'http://localhost:8080'
  }

  const appPathRoot = await getAppPathRoot()

  return path.join(appPathRoot, 'js')
}

export function isObject (item: any) {
  return (item === Object(item) && !Array.isArray(item))
}

export function deepMerge (
  target: IObject,
  ...sources: Array<IObject>
) {
  // return the target if no sources passed
  if (!sources.length) {
    return target
  }

  const result: IObject = target

  if (isObject(result)) {
    const len: number = sources.length

    for (let i = 0; i < len; i += 1) {
      const elm: any = sources[i]

      if (isObject(elm)) {
        for (const key in elm) {
          if (elm.hasOwnProperty(key)) {
            if (isObject(elm[key])) {
              if (!result[key] || !isObject(result[key])) {
                result[key] = {}
              }
              deepMerge(result[key], elm[key])
            } else {
              if (Array.isArray(result[key]) && Array.isArray(elm[key])) {
                // concatenate the two arrays and remove any duplicate primitive values
                result[key] = Array.from(new Set(result[key].concat(elm[key])))
              } else {
                result[key] = elm[key]
              }
            }
          }
        }
      }
    }
  }

  return result
}

export function genID () {
  // Math.random should be unique because of its seeding algorithm.
  // Convert it to base 36 (numbers + letters), and grab the first 9 characters
  // after the decimal.
  return '_' + Math.random().toString(36).substr(2, 9)
}

export function ucFirst (str: string) {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

export function withFileProtocol (path: string) {
  if (!path) return ''
  const reg = /^(http|file|assets)/

  if (!reg.test(path)) {
    path = 'file://' + path
  }

  return path
}

/**
 * @param timeout milliseconds
 * @param tag string
 */
export function deferred<T = any> (timeout?: number, tag?: string) {
  let resolve: any, reject: any
  let settled = false
  const timeFn = (r: Function) => {
    return (v: T) => {
      timeout && clearTimeout(timeout)
      r(v)
      settled = true
    }
  }

  const promise = new Promise<T>((resolve1, reject1) => {
    resolve = timeFn(resolve1)
    reject = timeFn(reject1)

    if (timeout) {
      // @ts-ignore
      timeout = setTimeout(() => reject(new Error(`[deferred timeout] ${tag}`)), timeout)
    }
  })

  return {
    created: Date.now(),
    setTag: (t: string) => tag = t,
    resolve, reject, promise,
    get settled () {
      return settled
    }
  }
}

export function invokeHostExportedApi (
  method: string,
  ...args: Array<any>
) {
  const method1 = snakeCase(method)

  // @ts-ignore
  const logseqHostExportedApi = window.logseq?.api || {}

  const fn = logseqHostExportedApi[method1] || window.apis[method1] ||
    logseqHostExportedApi[method] || window.apis[method]

  if (!fn) {
    throw new Error(`Not existed method #${method}`)
  }
  return typeof fn !== 'function' ? fn : fn.apply(null, args)
}

export function setupIframeSandbox (
  props: Record<string, any>,
  target: HTMLElement
) {
  const iframe = document.createElement('iframe')

  iframe.classList.add('lsp-iframe-sandbox')

  Object.entries(props).forEach(([k, v]) => {
    iframe.setAttribute(k, v)
  })

  target.appendChild(iframe)

  return async () => {
    target.removeChild(iframe)
  }
}

export function setupInjectedStyle (
  style: StyleString,
  attrs: Record<string, any>
) {
  const key = attrs['data-injected-style']
  let el = key && document.querySelector(`[data-injected-style=${key}]`)

  if (el) {
    el.textContent = style
    return
  }

  el = document.createElement('style')
  el.textContent = style

  attrs && Object.entries(attrs).forEach(([k, v]) => {
    el.setAttribute(k, v)
  })

  document.head.append(el)

  return () => {
    document.head.removeChild(el)
  }
}

export function setupInjectedUI (
  this: PluginLocal,
  ui: UIOptions,
  attrs: Record<string, any>
) {
  const pl = this
  let selector = ''

  if ('slot' in ui) {
    selector = `#${ui.slot}`
  } else {
    selector = ui.path
  }

  const target = selector && document.querySelector(selector)
  if (!target) {
    console.error(`${this.debugTag} can not resolve selector target ${selector}`)
    return
  }

  const key = `${ui.key}-${pl.id}`

  let el = document.querySelector(`div[data-injected-ui="${key}"]`) as HTMLElement

  if (el) {
    el.innerHTML = ui.template
    return
  }

  el = document.createElement('div')
  el.dataset.injectedUi = key || ''

  // TODO: Support more
  el.innerHTML = ui.template

  attrs && Object.entries(attrs).forEach(([k, v]) => {
    el.setAttribute(k, v)
  })

  target.appendChild(el);

  // TODO: How handle events
  ['click', 'focus', 'focusin', 'focusout', 'blur', 'dblclick',
    'keyup', 'keypress', 'keydown', 'change', 'input'].forEach((type) => {
    el.addEventListener(type, (e) => {
      const target = e.target! as HTMLElement
      const trigger = target.closest(`[data-on-${type}]`) as HTMLElement
      if (!trigger) return

      const msgType = trigger.dataset[`on${ucFirst(type)}`]
      msgType && pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
    }, false)
  })

  return () => {
    target!.removeChild(el)
  }
}

export function transformableEvent (target: HTMLElement, e: Event) {
  const obj: any = {}

  if (target) {
    const ds = target.dataset
    const FLAG_RECT = 'rect'

    ;['value', 'id', 'className',
      'dataset', FLAG_RECT
    ].forEach((k) => {
      let v: any

      switch (k) {
        case FLAG_RECT:
          if (!ds.hasOwnProperty(FLAG_RECT)) return
          v = target.getBoundingClientRect().toJSON()
          break
        default:
          v = target[k]
      }

      if (typeof v === 'object') {
        v = { ...v }
      }

      obj[k] = v
    })
  }

  return obj
}

let injectedThemeEffect: any = null

export function setupInjectedTheme (url?: string) {
  injectedThemeEffect?.call()

  if (!url) return

  const link = document.createElement('link')
  link.rel = 'stylesheet'
  link.href = url
  document.head.appendChild(link)

  return (injectedThemeEffect = () => {
    document.head.removeChild(link)
    injectedThemeEffect = null
  })
}
